퍼플심_04_아키텍쳐 정리하기
들어가는 말
MVP 기능 구현은 어느 정도 일단락됐다.
이번에는 이 시스템이 어떻게 돌아가는지 정리해보려 한다.
나중에 유지보수할 '미래의 나'를 위한 기술 문서이기도 하다.
1. Core Stack: "No Database"
DB 없는 100% 클라이언트 사이드 앱이다.
- Framework: Next.js(App Router)
- Language: TypeScript(Strict Mode)
- State: Zustand
- Storage: LocalStorage(진행 상황 저장용, 예정)
교육용 시뮬레이터 특성상 사용자마다 독립된 인스턴스가 필요했다. 또 복잡한 백엔드 로직보다 '인터랙션'이 더 중요하다고 판단했다.
그래서 무거운 백엔드 대신 브라우저 메모리 안에서 거대한 상태 머신(State Machine)이 돌아가는 구조를 택했다.
물론 시나리오가 많아지고 상태가 복잡해지면 이 구조도 문제가 될 수 있다.
이 점은 염두에 두고 있다. 나중에 필요하면 백엔드로 이전할 수 있도록 설계할 것이다.
2. State Management: Zustand Engine
이 앱의 심장은 useScenarioStore다.
단순히 "지금 몇 단계인가?"만 저장하는 게 아니다. 시뮬레이션의 모든 상태를 관리하는 엔진으로 구성했다.
interface ScenarioState {
// Game State
currentRole: 'RED' | 'BLUE';
currentPhaseIndex: number;
// Blue Team Progress (Complex Object)
blueProgress: {
detectedArtifacts: Artifact[]; // 발견한 증거
executedActions: string[]; // 수행한 조치
blockedIPs: string[]; // 차단한 IP
// ...
};
}
레드팀은 선형적(Linear)이라 currentPhaseIndex만 올리면 되지만
대응을 해야 하는 블루팀은 비선형적이라 blueProgress 객체 안에 수행한 행동들을 누적해서 관리한다.
이 모든 게 Zustand 스토어 하나에서 관리되므로 컴포넌트 간 데이터 동기화 걱정이 없다.
추후 시나리오의 레드팀 작업 방식에 따라 비선형적으로 다시 설계해야할 수 있다.
3. Scenario as Code
시나리오 데이터(react2shell.ts)는 JSON이 아니라 TypeScript 파일로 관리한다.
타입 안정성(Type Safety)을 위해 이런 방식을 많이 쓴다고 해서 한 번 써보기로 했다.
시나리오 흐름이 복잡해질수록 데이터 오타 하나가 치명적인 버그가 되니까.
export const react2shell: Scenario = {
id: "react2shell",
title: "React -> Shell",
phases: [
{
role: "RED",
type: "RECONNAISSANCE",
// ...
},
{
role: "BLUE",
// ...
}
]
};
이렇게 코드로 관리하면 컴파일 타임에 에러를 잡을 수 있고 IDE의 자동 완성 지원도 받을 수 있다.
나중에 시나리오가 100개가 되어도 이 구조라면 관리가 쉬울 것이다.
4. Role Separation(One Store, Two Views)
하나의 스토어를 쓰지만 뷰(View)는 철저히 분리했다.
- RedTeamWorkbench: 공격자용 UI (터미널, 에디터 중심)
- BlueTeamWorkbench: 방어자용 UI (대시보드, 분석 도구 중심)
currentRole 상태값에 따라 렌더링되는 최상위 컴포넌트 자체가 갈아끼워지는 방식이다.
덕분에 코드가 섞이지 않고 각 역할에 특화된 UI/UX를 마음껏 구현할 수 있었다.
홈 랜딩 페이지도 좀 꾸몄다.
